Skip to content

fix: preload css for nested dynamic imports (fix #22700)#22721

Closed
mturac wants to merge 3 commits into
vitejs:mainfrom
mturac:fix/issue-22700-preload-marker-pairing
Closed

fix: preload css for nested dynamic imports (fix #22700)#22721
mturac wants to merge 3 commits into
vitejs:mainfrom
mturac:fix/issue-22700-preload-marker-pairing

Conversation

@mturac

@mturac mturac commented Jun 19, 2026

Copy link
Copy Markdown

Description

Fixes #22700.

Nested dynamic imports could reuse a preload marker that had already been paired with an earlier import. When that happened, the outer import could lose its dependency list and the CSS for the imported chunk would not be loaded in production builds.

This change skips already-consumed preload markers before rewriting the marker for the current dynamic import.

The regression test exercises the build-only behavior in the dynamic-import playground by checking that the nested import creates a stylesheet link and applies the imported CSS.

Tests

  • pnpm run build
  • pnpm --filter vite run build
  • pnpm run test-build dynamic-import -t "should preload css for nested dynamic imports"
  • pnpm run test-build dynamic-import
  • pnpm exec eslint packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/nested/index.js playground/dynamic-import/nested/issue-22700-a.js playground/dynamic-import/nested/issue-22700-b.js
  • pnpm exec oxfmt --check packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/index.html playground/dynamic-import/nested/index.js playground/dynamic-import/nested/issue-22700-a.js playground/dynamic-import/nested/issue-22700-b.js playground/dynamic-import/nested/issue-22700-a.css
  • git diff --check

@shulaoda shulaoda self-assigned this Jun 23, 2026
@shulaoda

Copy link
Copy Markdown
Member

Thanks for digging into this. It does make the CSS load again, but the pairing it produces attaches each import's deps to the wrong marker.

For import('a').then(() => import('b')), Rolldown wraps the whole import().then(), so b's marker comes before a's in the text. Walking the imports from front to back, import('a') takes the first marker after it, which is actually b's, and then import('b') finds that marker already taken and falls through to the outer one. The two dep lists end up swapped:

__vitePreload(
  () =>
    import('a').then(() =>
      __vitePreload(() => import('b') /* a's deps */),
    ) /* b's deps */,
)

a's CSS is no longer dropped, which fixes the visible bug, but it now sits on the inner __vitePreload. That inner call only runs after a has loaded and its .then fires, so a's stylesheet is fetched late instead of alongside a.js, and each call's preload list points at the other chunk's assets. With three or more levels the dep lists rotate instead of swapping.

@mturac mturac force-pushed the fix/issue-22700-preload-marker-pairing branch from 72cd357 to ff84d70 Compare June 24, 2026 10:23
@mturac

mturac commented Jun 24, 2026

Copy link
Copy Markdown
Author

Thanks, that makes sense. I updated the branch to pair preload markers from the inside out by processing dynamic imports in reverse order.

I also tightened the build test so it checks the generated output shape: the outer import("./issue-22700-a") preload gets the dep map, while the inner import("./issue-22700-b") preload gets [].

Validated locally:

  • pnpm --filter vite build
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts -t "should preload css for nested dynamic imports"
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm exec oxfmt --check packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/nested/index.js
  • pnpm exec eslint packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm --filter vite typecheck

@shulaoda

Copy link
Copy Markdown
Member

Nice, the backwards + skip-claimed version pairs them correctly now. Each nested import gets its own deps, so #22700 is fixed and the swap is gone. I ran it against the original repro and a few nesting shapes, CSS lands on the right call.

Two tiny things, both minor:

  1. The backward loop re-scans the inner markers for every enclosing import (each findPreloadMarker walks past the already-claimed ones), so it's ~O(n²) on deep nesting. Doesn't matter for real code, just flagging it. A single forward pass with a stack scans once.

  2. Looping backwards also fills addFileDep in reverse, so __vite__mapDeps comes out reversed (the indices shift with it). Same at runtime, but the output changes, so it might move some snapshots.

Also tweaked the test a bit. The build regex pins the exact minified shape (the arrows and the \1 backref), so it'll break whenever the minifier output shifts. ,[] and __vite__mapDeps are injected after minify, so you can just check the inner import's slot:

const js = findAssetFile(/index-[-\w]{8}\.js$/) ?? ''
const innerPreload = js.match(
  /import\([^)]*issue-22700-b-[-\w]+\.js[^)]*\)[\s\S]*?(,\[\]\)|__vite__mapDeps)/,
)?.[1]
expect(innerPreload).toBe(',[])')

shulaoda
shulaoda previously approved these changes Jun 24, 2026

@shulaoda shulaoda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. cc @sapphi-red, both this and #22759 fix it and this one's a smaller diff. Left a couple of small notes above.

@shulaoda shulaoda requested a review from sapphi-red June 24, 2026 23:55

@sapphi-red sapphi-red left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking so that it's clear that I'm reviewing this and #22759

@mturac

mturac commented Jun 28, 2026

Copy link
Copy Markdown
Author

Thanks for the follow-up. I updated this to pair preload markers in a single forward scan: marker positions are assigned to the latest open dynamic import with a stack, while keeping the #3051 single-import fallback. This also keeps import processing forward, so addFileDep order stays stable.

Validated locally:

  • pnpm exec oxfmt --check packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts playground/dynamic-import/nested/index.js
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts -t "should preload css for nested dynamic imports"
  • VITE_TEST_BUILD=1 pnpm vitest run -c vitest.config.e2e.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm exec eslint packages/vite/src/node/plugins/importAnalysisBuild.ts playground/dynamic-import/__tests__/dynamic-import.spec.ts
  • pnpm --filter vite build
  • pnpm --filter vite typecheck
  • git diff --check HEAD~1 HEAD

@mturac

mturac commented Jun 29, 2026

Copy link
Copy Markdown
Author

CI note: the remaining Windows node-24 failure looks unrelated to this change. The unit run hit a libuv fs-event.c assertion and Vitest worker exit after 61/62 test files and 832/837 tests had passed. The changed dynamic-import build test is not the failing area, and the Linux/macOS jobs are green.

I tried to rerun the failed job, but GitHub requires repository admin rights for that, so this likely needs a maintainer rerun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build: nested import().then(() => import()) drops outer __vitePreload deps to void 0, orphaning CSS

3 participants